手把手教你vite+elementPlus+vue3+jsplumb+pinia+sortable实现完整的流程图 | 您所在的位置:网站首页 › canvas 流程图 › 手把手教你vite+elementPlus+vue3+jsplumb+pinia+sortable实现完整的流程图 |
1、vite创建项目
yarn create vite
2、安装vue-router
npm install vue-router
1、新建一个router的文件夹
2、在文件夹下面创建一个index.js文件
3、main.js进行挂载
index.js
import { createRouter, createWebHistory } from 'vue-router'
import home from '../view/home.vue'
const routerHistory = createWebHistory()
const router = createRouter({
history: routerHistory,
routes: [
{
path: '/',
redirect:'home'
},
{
path: '/home',
name:'home',
component: home
},
]
})
export default router;
main.js挂在router
import { createApp } from 'vue'
import App from './App.vue'
//路由
import router from './router'
createApp(App).use(router).use(store).use(ElementPlus).mount('#app')
app.vue改造
3、安装pinia
npm i pinia -S
1、新建 src/store 目录并在其下面创建 index.js,导出 store
import { createPinia } from 'pinia'
const store = createPinia()
export default store
2、在 main.js 中引入并使用
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
import router from "./router";
createApp(App).use(router).use(store).mount('#app')
3、在 src/store 下面创建一个 confgJsplumb.js
import { defineStore } from 'pinia'
export const configJsplumb = defineStore({
id: 'confgJsplumb', // id必填,且需要唯一
state: () => {
return {
name: '张三'
}
},
actions: {
updateName(name) {
this.name = name
}
}
})
4、获取State
{{configJsplumbData.name}}
import { configJsplumb } from '../store/configJsplumb.js'
let configJsplumbData = configJsplumb()
console.log(configJsplumbData.name)
5、修改值
{{configJsplumbData.name}}
import { configJsplumb } from '../store/configJsplumb.js'
let configJsplumbData = configJsplumb()
configJsplumbData.updateName('李四')
4、安装less或者scss
yarn add -D sass
npm install sass -D
yarn add -D less
npm install less -D
5、集成eslint和prettier
npm install --save-dev eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue eslint-plugin-html prettier @babel/plugin-syntax-dynamic-import babel-eslint
创建配置文件: `.eslintrc.js`
module.exports = {
root: true,
env: {
node: true,
'vue/setup-compiler-macros': true,
},
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'prettier',
'plugin:prettier/recommended',
],
plugins: ['vue', 'html', 'prettier'],
parserOptions: {
ecmaVersion: 8,
},
rules: {
'prettier/prettier': 'error',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/no-multiple-template-root': 'off',
'vue/multi-word-component-names': 'off',
'no-mutating-props': 'off',
'vue/no-v-html': 'off',
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)',
],
env: {
mocha: true,
},
},
],
};
创建忽略文件:`.eslintignore`
node_modules/
dist/
index.html
创建配置文件: `prettier.config.js` 或 `.prettierrc.js`
module.exports = {
// 一行最多 80 字符
printWidth: 80,
// 使用 4 个空格缩进
tabWidth: 4,
// 不使用 tab 缩进,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号代替双引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾使用逗号
trailingComma: 'all',
// 大括号内的首尾需要空格 { foo: bar }
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// 换行符使用 lf
endOfLine: 'auto'
}
6、安装eslint错误显示信息依赖
vite-plugin-eslint
vite.config.js修改
import { defineConfig } from 'vite';
import eslintPlugin from 'vite-plugin-eslint';
export default defineConfig({
plugins: [eslintPlugin()],
});
7、安装element-plus
npm install --save element-plus
import { createApp } from 'vue';
import App from './App.vue';
import store from './store';
import router from './router';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
createApp(App).use(router).use(store).use(ElementPlus).mount('#app');
8、流程图整体功能和实现效果以及代码结构图
9、安装sortable实现左侧列表拖拽
npm install sortablejs --save
//代码演示
//left.vue
{{ item.name }}
export default {
name: 'Left',
};
import { onMounted } from 'vue';
import Sortable from 'sortablejs';
import { configJsplumb } from '../../store/configJsplumb.js';
let tableData = [
{
type: 'statr',
name: '开始',
color: '#479A52',
},
{
type: 'node',
name: '节点',
color: '#5a9cf8',
},
{
type: 'end',
name: '结束',
color: 'red',
},
];
let configJsplumbData = configJsplumb();
onMounted(() => {
// 第一步,获取行容器
let dome = document.getElementById('leftUl');
// 第二步,初始化,给容器指定对应拖拽规则
new Sortable(dome, {
group: 'shared',
animation: 150,
sort: false, //关闭在盒内可以拖拽
forceFallback: true,//忽略 HTML5拖拽行为,强制回调进行
ghostClass: 'blue-background-class', //drop placeholder的css类名
//结束拖拽
onEnd: function (evt) {
//当元素移动的距离左侧小于309px的时候并且距离顶部的距离小于69px的时候,禁止插入
if (evt.originalEvent.x < 309 && evt.originalEvent.y < 69) return;
let list = {
id: setSessionid(),//生成随机Id
x: evt.originalEvent.x - 310,//落下的位置(310是左侧列表整体的宽度)
y: evt.originalEvent.y - 70,//落下的位置(70是头部的宽度)
};
//根据当前数据的下标拿到对应数据的,并和list合并
Object.assign(list, tableData[evt.newIndex]);
//并使用pinia来存储数据
configJsplumbData.updateSortData(list);
},
});
});
//生成随机id
const setSessionid = () => {
var len = 32;
var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
var maxPos = chars.length;
var pwd = '';
for (var i = 0; i < len; i++) {
pwd += chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
};
#leftUl {
height: 100%;
box-sizing: border-box;
padding: 0 5px;
list-style: none;
li {
text-align: center;
padding: 5px 10px;
border: 1px solid #eaeaea;
margin-bottom: 3px;
font-size: 14px;
cursor: pointer;
background: white;
&:first-child {
margin-top: 3px;
}
}
}
10、pinia里面的代码
import { defineStore } from 'pinia';
export const configJsplumb = defineStore({
id: 'configJsplumb', // id必填,且需要唯一
state: () => {
return {
jslumbSortData: [],
};
},
actions: {
//拿到数据就插入,假如大家要做一些插入的时候,判断是否重复就可以在这里做
updateSortData(name) {
this.jslumbSortData.unshift(name);
},
},
});
11、引入jsplumb
npm install --save jsplumb
//引入
import {ref, reactive,onMounted} from 'vue'
import jsPlumb from 'jsplumb'
12、右键菜单工具
npm i contextmenu-vue@next
const handleContextMenu = ($event) => {
$contextmenu({
x: $event.x,
y: $event.y,
customLayoutClass: 'customLayoutClass',
menuList: [
{
text: '删除',
icon: 'DeleteFilled',
onClick: () => {},
},
{
text: '复制',
icon: 'DocumentCopy',
onClick: () => {},
},
{
text: '多选删除',
icon: 'IceCream',
onClick: () => {},
},
{
text: '多选复制',
icon: 'Link',
onClick: () => {},
},
{
text: '多选拖拽',
icon: 'Location',
onClick: () => {},
},
],
});
};
//右键菜单消失
const rightClick = () => {
document.querySelectorAll('.customLayoutClass')[0].style.display = 'none';
};
13、编写jsplumb配置规则
1、新建一个unti文件夹
2、创建commonConfig.js文件
export const jsplumbSetting = {
grid: [10, 10],
// 动态锚点、位置自适应
Anchors: ['TopCenter', 'RightMiddle', 'BottomCenter', 'LeftMiddle'],
Container: 'center', //可拖动区域 ID
// 连线的样式 StateMachine、Flowchart,有四种默认类型:Bezier(贝塞尔曲线),Straight(直线), Flowchart(流程图),State machine(状态机)
Connector: [
'Flowchart',
{ cornerRadius: 5, alwaysRespectStubs: true, stub: 5 },
],
// 鼠标不能拖动删除线
ConnectionsDetachable: true,
// 删除线的时候节点不删除
DeleteEndpointsOnDetach: false,
// 连线的端点类型(Dot 圆点、Rectangle 矩形、Image 图像、Blank 空白)
// Endpoint: ["Dot", {radius: 5}],
Endpoint: [
'Rectangle',
{
height: 10,
width: 10,
},
],
// 线端点的样式
EndpointStyle: {
fill: 'red', //端点背景色
outlineWidth: 1, //端点宽度
outlineStroke: 'red', //端点颜色
radius: 5,
}, //这个是控制连线终端那个小点的样式
EndpointHoverStyle: { fill: '#fff', outlineStroke: '#13E0F9' }, //这个是控制鼠标滑过连线终端那个小点的样式
LogEnabled: false, //是否打开jsPlumb的内部日志记录
// 绘制线
PaintStyle: {
stroke: '#409eff',
strokeWidth: 2, //线的宽度
},
HoverPaintStyle: { stroke: '#ff00cc', strokeWidth: 2 }, //鼠标滑过连线的位置
// 绘制箭头
Overlays: [
[
'Arrow',
{
width: 8,
length: 8,
location: 1,
},
],
],
RenderMode: 'svg',
};
// jsplumb连接参数
export const jsplumbConnectOptions = {
isSource: true,
isTarget: true,
// 动态锚点、提供了4个方向 Continuous、AutoDefault
anchor: ['TopCenter', 'RightMiddle', 'BottomCenter', 'LeftMiddle'],
};
//开始瞄点
export const jsplumbSourceOptions = {
filter: '.node-anchor', //触发连线的区域
/*"span"表示标签,".className"表示类,"#id"表示元素id*/
filterExclude: false,
anchor: ['TopCenter', 'RightMiddle', 'BottomCenter', 'LeftMiddle'],
allowLoopback: false,//防止往回连接
};
//结束瞄点
export const jsplumbTargetOptions = {
filter: '.node-anchor',
/*"span"表示标签,".className"表示类,"#id"表示元素id*/
filterExclude: false,
anchor: ['TopCenter', 'RightMiddle', 'BottomCenter', 'LeftMiddle'],
allowLoopback: false,//防止往回连接
};
14、初始化jsplumb并引入配置文件
//4个连接点的位置,瞄点
//节点名称
{{ item.name }}
export default {
name: 'Center',
};
import { reactive, toRefs, onMounted, nextTick } from 'vue';
//右键菜单
import { $contextmenu } from 'vue-contextmenu-next/index';
//随机id
import { GenNonDuplicateID } from '../../unti/unti.js';
//jsplumbp配置
import {
jsplumbSetting,
jsplumbConnectOptions,
jsplumbSourceOptions,
jsplumbTargetOptions,
} from '../../unti/commonConfig.js';
//引入jsplumb实例
import jsPlumb from 'jsplumb';
let $jsPlumb = jsPlumb.jsPlumb;
// 缓存实例化的jsplumb对象
let jsPlumb_instance = null;
// let configJsplumbData = configJsplumb();
let data = reactive({
jsplumbSettingConfig: jsplumbSetting,
jsplumbConnectOptionsConfig: jsplumbConnectOptions,
jsplumbSourceOptionsConfig: jsplumbSourceOptions,
jsplumbTargetOptionsConfig: jsplumbTargetOptions,
nodeList: [],
lineList: [],
});
let {
nodeList, //节点数据
lineList, //连线数组
jsplumbTargetOptionsConfig,
jsplumbSourceOptionsConfig,
jsplumbConnectOptionsConfig,
jsplumbSettingConfig,
} = toRefs(data);
console.log(
jsplumbSettingConfig,
jsplumbConnectOptionsConfig,
jsplumbSourceOptionsConfig,
jsplumbTargetOptionsConfig,
);
onMounted(() => {
//jsplumb默认是注册在浏览器窗口的,将整个页面提供给我们作为一个实例,所以可以使用 getInstance方法建立页面中一个独立的实例:
jsPlumb_instance = $jsPlumb.getInstance();
nodeList.value = [
{
id: 'Mfzcx4apkafHBYATbdz6hFYntSHsXtDz',
x: 335,
y: 103,
type: 'node',
name: 'kaishi1',
color: '#5a9cf8',
},
{
id: 'sGsGhyhEJBsCSmRaAw8xJK3iaefcrKDX',
x: 833,
y: 79,
type: 'statr',
name: '开始',
color: '#479A52',
},
{
id: 'sGsGhyhEJBsCSmRaAw8xJK3iaefcrKDl',
x: 833,
y: 150,
type: 'statr',
name: '开始',
color: '#479A52',
},
{
id: 'sGsGhyhEJBsCSmRaAw8xJK3iaefcrKDl11',
x: 833,
y: 300,
type: 'statr',
name: '开始11111',
color: '#479A52',
},
];
nextTick(() => {
//因为jsplimb要在dom元素加载之后才能执行,因为他的原理是找到你绑定的画布dom去渲染svg数据,所以必须得画布dom已经渲染之后才能初始化
init();
});
});
//初始化
const init = () => {
//ready监听jsplumb是否实例化,当jsplumb实例化执行ready内的方法,
jsPlumb_instance.ready(() => {
//给jsplumb设置一些默认值
jsPlumb_instance.importDefaults(jsplumbSettingConfig.value);
loadEasyFlow();
// 会使整个jsPlumb立即重绘。
jsPlumb_instance.setSuspendDrawing(false, true);
});
};
//加载流程图-初始化节点,使节点可以拖拽
const loadEasyFlow = () => {
// 初始化节点
for (let i = 0; i < nodeList.value.length; i++) {
let node = nodeList.value[i];
// 设置源点,可以拖出线连接其他节点
jsPlumb_instance.makeSource(node.id, jsplumbSourceOptionsConfig.value);
// // 设置目标点,其他源点拖出的线可以连接该节点
jsPlumb_instance.makeTarget(node.id, jsplumbTargetOptionsConfig.value);
// this.jsPlumb.draggable(node.id);
//画布节点添加拖拽方法
draggableNode(node.id);
jsPlumb_instance.unbind('connection'); //取消连接事件
for (let i = 0; i < lineList.value.length; i++) {
let line = this.data.lineList[i];
jsPlumb_instance.connect(
{
source: line.from,
target: line.to,
},
jsplumbConnectOptionsConfig.value,
);
}
//连线
jsPlumb_instance.bind('connection', (evt) => {
let from = evt.source.id;
let to = evt.target.id;
lineList.value.push({
from: from,
to: to,
label: '',//连线名称
id: GenNonDuplicateID(8),
Remark: '',
});
});
}
};
//给画布节点添加拖拽方法
const draggableNode = (nodeId) => {
console.log(nodeId, 'nodeId');
jsPlumb_instance.draggable(nodeId, {
grid: [5, 5],
containment: 'center',
drag: (event) => {
window.event
? (window.event.cancelBubble = true)
: event.stopPropagation();
return false;
},
stop: (event) => {
let nodeIndex = nodeList.value.findIndex((x) => x.id === nodeId);
Object.assign(nodeList.value[nodeIndex], {
x: event.pos[0],
y: event.pos[1],
});
},
});
};
//右键菜单
const handleContextMenu = ($event) => {
$contextmenu({
x: $event.x,
y: $event.y,
customLayoutClass: 'customLayoutClass',
menuList: [
{
text: '删除',
icon: 'DeleteFilled',
onClick: () => {},
},
{
text: '复制',
icon: 'DocumentCopy',
onClick: () => {},
},
{
text: '多选删除',
icon: 'IceCream',
onClick: () => {},
},
{
text: '多选复制',
icon: 'Link',
onClick: () => {},
},
{
text: '多选拖拽',
icon: 'Location',
onClick: () => {},
},
],
});
};
//右键菜单消失
const rightClick = () => {
document.querySelectorAll('.customLayoutClass')[0].style.display = 'none';
};
watch(
configJsplumbData.jslumbSortData,
() => {
if (configJsplumbData.jslumbSortData.length > 0) {
let list = configJsplumbData.jslumbSortData[0];
nodeList.value.push(list);
//从左侧拖拽添加节点,添加新的节点后,要重绘画布
nextTick(() => {
jsPlumb_instance.makeSource(
list.id,
jsplumbSourceOptionsConfig.value,
);
jsPlumb_instance.makeTarget(
list.id,
jsplumbConnectOptionsConfig.value,
);
draggableNode(list.id);
});
}
},
{
deep: true,
immediate: true,
},
);
#centerRelative {
position: relative;
width: 100%;
height: 100%;
.item {
position: absolute;
text-align: center;
padding: 5px 10px;
border: 1px solid #eaeaea;
width: 180px;
height: 20px;
font-size: 14px;
cursor: pointer;
background: white;
}
}
.customLayoutClass {
width: 152px !important;
.context-menu-item {
font-size: 13px;
height: 29px;
padding: 0;
.arrow {
.icon {
width: 0.7rem;
}
}
}
}
.node-anchor {
width: 10px;
height: 10px;
border: 1px solid red;
z-index: 333;
}
.anchor-top {
position: absolute;
top: 0;
left: 0;
margin-left: 48%;
margin-top: -6px;
}
.anchor-bottom {
position: absolute;
bottom: 0;
left: 0;
margin-left: 48%;
margin-bottom: -6px;
}
.anchor-right {
position: absolute;
right: 0;
margin-right: -7px;
}
.anchor-left {
position: absolute;
left: 0;
margin-left: -7px;
}
15、删除节点
jsPlumb_instance.remove(id)
//删除
const deleteBtn = (item, index) => {
let isState = nodeList.value.some((x) => x.id === item.id);
if (isState) {
nodeList.value.splice(index, 1);
jsPlumb_instance.remove(item.id);
rightClick();
return true;
} else {
return false;
}
};
16、复制节点 //复制节点 const copyNodeBtn = (item) => { let list = JSON.parse(JSON.stringify(item)); list.id = GenNonDuplicateID(8); list.x = item.x + 100; nodeList.value.push(list); nextTick(() => { jsPlumb_instance.makeSource(list.id, jsplumbSourceOptionsConfig.value); jsPlumb_instance.makeTarget(list.id, jsplumbConnectOptionsConfig.value); draggableNode(list.id); }); }; 提示:多选复制和多选删除其实和复制节点、删除节点方法一样,暂不做演示,循环与不循环的区别 17、连线前的校验事件 //完成连线前的校验 jsPlumb_instance.bind('beforeDrop', (evt)=>{}) jsPlumb_instance.bind('beforeDrop', (evt) => { //true代表可以连接 false代表可以连接 return beforeDrop(evt); });true代表可以连接 false代表不可以连接 这里常常一般用于两个节点连接前要做的一些判断 18、连线成功后的事件 jsPlumb_instance.bind('connection', (evt) => {}) jsPlumb_instance.bind('connection', (evt) => { let from = evt.source.id; let to = evt.target.id; lineList.value.push({ from: from, to: to, label: '', id: GenNonDuplicateID(8), Remark: '', }); }); 19、双击连接线 jsPlumb_instance.bind("dblclick",(conn, originalEvent) => {}) jsPlumb_instance.bind("dblclick",(conn, originalEvent) => { console.log(conn,originalEvent) }) 20、单击连接线 jsPlumb_instance.bind('click', (conn, originalEvent)=>{}) jsPlumb_instance.bind('click', (conn, originalEvent) => { console.log(conn, 'conn'); console.log(originalEvent, 'originalEvent'); }); 21、单击删除连接线 jsPlumb_instance.deleteConnection(对象) jsPlumb_instance.bind('click', (conn, originalEvent) => { jsPlumb_instance.deleteConnection(conn); lineList.value.forEach((item, index) => { if (item.from === conn.sourceId && item.to === conn.targetId) { this.data.lineList.splice(index, 1); } }); }); 22、断开连线后事件 jsPlumb_instance.bind('connectionDetached', (evt) => { console.log(evt, '1111'); }); 23、更改连线的样式 .jtk-connector.active { z-index: 9999; path { stroke: #150042; stroke-width: 1.5; animation: ring; animation-duration: 3s; animation-timing-function: linear; animation-iteration-count: infinite; stroke-dasharray: 5; } } @keyframes ring { from { stroke-dashoffset: 50; } to { stroke-dashoffset: 0; } } 24、全局控制连线的样式 // 添加虚线 //获取所有的连线数据 let lines = jsPlumb_instance.getAllConnections(); lines.forEach((line) => { line.canvas.classList.add('active'); }); //关闭虚线 let lines = jsPlumb_instance.getAllConnections(); lines.forEach((line) => { line.canvas.classList.remove('active'); }); 25、给连线加入文字或者html片段 //setLabel jsPlumb_instance.bind('click', (conn, originalEvent) => { conn.setLabel({ label: '122222', cssClass: '',location;'' }); conn.setLabel({ label: '123213 ', cssClass: '' }); console.log(conn, originalEvent); }); 26、获取所有的连线数据 jsPlumb_instance.getConnections() jsPlumb_instance.getAllConnections() getAllConnections(): getAllConnections返回的是浅拷贝的数组,对返回的连线数组进行循环遍历时,当遍历过程中删除了连线时,会破坏数组的循环。解决的办法是将返回的数组进行深拷贝再循环 getConnections(): getConnections返回的是新的数组,所以可以直接循环 27、多个节点一起拖拽 jsPlumb_instance.addToDragSelection(id); const chooseMoreDrag = () => { let idArray = nodeList.value.map((x) => x.id); for (let id of idArray) { jsPlumb_instance.addToDragSelection(id); } }; 28、取消多个节点拖拽 jsPlumb_instance.removeFromDragSelection(id); const chooseMoreDrag = () => { let idArray = nodeList.value.map((x) => x.id); for (let id of idArray) { jsPlumb_instance.removeFromDragSelection(id); } }; 29、节点可拖拽 //获取所有节点dom let elem = document.querySelectorAll('.item'); //区域范围内的节点可拖拽 jsPlumb_instance.draggable(elem, true); //节点是否可拖拽 jsPlumb_instance.setDraggable(elem, true); //可直接单独使用setDraggable 30、节点不可拖拽 //获取所有节点dom let elem = document.querySelectorAll('.item'); //区域范围内的节点可拖拽 jsPlumb_instance.draggable(elem, false); //节点是否可拖拽 jsPlumb_instance.setDraggable(elem, false); //可直接单独使用setDraggable 31、清空画布 document.querySelectorAll('画布id').html('') jsPlumb_instance.reset(); 32、删除所有连线 jsPlumb_instance.detachEveryConnection(); 33、vue2版本juejin.cn/post/706816… 34、git地址github.com/Caoxiongk/F… 结尾 码字不易,喜欢的小伙伴记得点赞关注 |
CopyRight 2018-2019 实验室设备网 版权所有 |